Appearance
04A. Tool 能力接口与执行边界
版本:
v1.2最后更新:
2026-07-09适用对象:想把 Agent 的“会调用工具”真正落到接口设计、权限边界、失败处理、运行时治理和评测体系里的产品、研发、平台与架构同学
1. 为什么要把 Tool 单独讲
很多团队说“我们已经接了很多工具”,但真正上线以后问题并不出在“有没有工具”,而出在:
- 工具定义太大,模型不知道什么时候该调
- 参数 schema 太松,调用看起来成功,业务实际上已经偏了
- 读工具、写工具、审批工具混在一起,风险边界不清
- 工具失败后只返回一段自由文本,系统没法稳定恢复
- 工具结果原样回灌,把后续推理一起污染
从工程视角看,Tool 不是“顺手接一个 API”这么简单,它是 Agent 系统里最小的可执行能力单元。
2. Tool 到底是什么
可以把 Tool 理解成一句话:
Tool = 一个被模型可见、被系统可控、被运行时真正执行的最小能力接口
它至少要回答六个问题:
- 这个能力什么时候该用
- 允许传什么参数
- 真正由谁执行
- 成功和失败分别返回什么
- 会不会产生真实副作用
- 调用结果如何影响下一轮推理
这里最关键的一点是:
- 模型负责提出调用建议
- 应用或运行时负责校验、审批和执行
也就是说,Tool 不是“模型自己在操作外部系统”,而是“模型在受控边界内发起一个动作请求”。
2.1 Tool calling 是一个多步闭环,不是单次函数调用
OpenAI 当前官方 Function Calling 指南把 tool calling 描述成一个多步对话闭环:
- 你把工具定义连同任务一起发给模型
- 模型返回 tool call
- 你的应用侧执行工具
- 你把 tool output 再发回模型
- 模型继续回答,或继续发起更多 tool call
这条闭环很重要,因为很多线上问题不是出在“模型有没有选对工具”,而是出在:
- tool output 怎么回灌
- 回灌后模型有没有继续误调
- 调用失败后系统有没有停住
2.2 Tool 不等于函数,函数只是 tool 的一种
在 OpenAI 当前工具体系里,至少要区分三类对象:
| 类型 | 输入形态 | 更适合什么 |
|---|---|---|
function tool | JSON schema | 参数结构明确、可验证、适合业务接口 |
custom tool | 自由文本或 grammar 约束文本 | 输入不适合硬包一层 JSON 的场景 |
built-in tool | 平台内建 | web search、MCP、shell、computer use 等 |
也就是说,函数只是 tool 的一个子类。
如果团队把所有工具都统称成“函数调用”,后面就会在:
- 参数校验
- 运行时所有权
- 审批边界
- 成本模型
这些地方一起混掉。
2.3 Tool 结果不只是“执行完了”,还是下一轮上下文的一部分
OpenAI 官方文档明确说明,tool output 会和:
- 工具定义
- 原始 prompt
- 模型 tool call
一起回到模型上下文中,供后续推理消费。
因此 Tool 的工程问题从来不只包括:
- 这个动作能不能做
还包括:
- 做完以后,什么结果该继续传播
- 什么结果该被清洗
- 什么结果根本不该再喂回模型
2.4 用同一个例子,把 Tool 单独看清
继续拿“事故分诊”举例:
Tool不负责定义整个事故流程Tool也不负责说明这些能力如何跨宿主暴露Tool只负责把某个动作做成稳定最小单元
例如:
read_recent_alerts(service, window_minutes)search_runbook(query, service)create_incident_ticket(severity, summary, evidence_ids)
这里你真正要讨论的是:
- 参数有没有冗余
- 输出是不是稳定结构
- 哪个动作有副作用
- 哪个动作必须审批
如果你开始讨论:
- 这些能力怎么按标准协议接到别的宿主
那你已经进入 MCP 话题了。
如果你开始讨论:
- 事故分诊这个任务应该先查告警、再读 runbook、最后决定是否建单
那你已经进入 Skill 或 workflow 话题了。
2.5 一个像样 Tool,最好先长成稳定契约
下面这个例子比“随手暴露后端接口”更接近模型真正适合调用的最小 Tool 面:
json
{
"type": "function",
"name": "create_incident_ticket",
"description": "在确认存在真实故障且证据充分后,创建事故工单。不要在只有猜测时调用。",
"strict": true,
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
"severity": {
"type": "string",
"enum": ["sev1", "sev2", "sev3"]
},
"summary": {
"type": "string",
"description": "面向值班同学的简短事故摘要"
},
"evidence_ids": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["severity", "summary", "evidence_ids"]
}
}这个例子真正重要的点不是 JSON 长什么样,而是它已经把几件事说清了:
- 什么时候该用,什么时候不该用
- 工单级别只能填哪些值
- 不能偷偷塞额外字段
- 证据是必填,而不是“感觉差不多就建单”
3. 一个成熟 Tool 的最小契约
一个能在线上长期运行的 Tool,通常至少要包含下面这些字段或等价信息:
| 维度 | 需要回答的问题 |
|---|---|
name | 模型怎么唯一识别它 |
description | 什么时候该用,什么时候不该用 |
input schema | 参数有哪些、哪些必填、允许什么枚举或格式 |
output schema | 成功时返回什么结构,失败时返回什么结构 |
side effects | 它是只读、写入、外呼、还是高风险动作 |
auth / approval | 哪些角色能用,哪些情况要审批 |
retry semantics | 超时后能不能自动重试,是否幂等 |
observability | 调用、参数、结果、耗时、失败原因如何记录 |
如果只剩下“函数名 + 参数列表”,那还不算一个成熟的 Tool 契约。
3.1 name 和 description 其实是工具发现层接口
很多团队只重视参数 schema,却低估了工具名和描述的重要性。
但模型一开始真正能依赖来做路由判断的,恰恰是:
namedescription
如果这两者不清楚,模型就容易:
- 误选相近工具
- 把写工具当读工具
- 在无关任务里滥用某个万能工具
3.2 input schema 和 output schema 都要当成正式契约
线上常见误区是:
- 输入很严格
- 输出靠自由文本“解释一下”
这会直接导致:
- 下游节点无法稳定消费
- 结果分类和回放困难
- 评测口径不稳定
更稳的做法通常是把以下三者一起管住:
- 输入 schema
- 成功输出 schema
- 错误输出 schema
3.3 call_id、namespace 和结果关联字段不能丢
OpenAI 当前官方 Function Calling 指南里,tool output 必须能关联到具体 tool call。
这意味着在工程实现上,至少要保留:
call_id- tool
name - 如有命名空间,还要保留
namespace
如果这层关联丢了,后面会很难回答:
- 这条结果是哪次调用产生的
- 哪个 tool call 失败了
- 某轮多工具并发时到底是谁污染了结果
4. Tool 设计最容易踩错的三件事
4.1 把后端 API 原样暴露给模型
后端接口通常是给工程师调的,不是给模型调的。常见问题有:
- 参数太多
- 同一个接口既能查又能改
- 错误码只对后端同学友好
- 返回结果太脏,下一轮模型很难稳定消费
更稳的做法通常是:
- 给模型重做一层更小、更清晰的 Tool 面
- 把复杂业务接口收敛成少量原子动作
- 把内部字段名和外部能力名分开
4.2 把读工具和写工具混在同一层暴露
读工具和写工具的治理要求完全不同:
- 读工具更关注召回、过滤、成本和噪音
- 写工具更关注审批、幂等、补偿和审计
如果在同一个任务阶段把两类工具一起放给模型,通常会导致:
- 非必要写入动作过早暴露
- 模型在多个相近工具之间误选
- 审批链被迫后置
4.3 只约束参数,不约束结果
很多团队会认真写输入 schema,却放任工具返回一大段自由文本。结果是:
- 下一轮模型被污染
- trace 很难复盘
- 工作流节点无法做稳定分支判断
所以 Tool 契约至少要同时治理:
- 输入结构
- 输出结构
- 错误结构
5. Function tool、custom tool 和 built-in tool 怎么选
5.1 Function tool 适合参数清晰、动作边界稳定的能力
如果你的工具输入天然是结构化字段,例如:
customer_idticket_idrefund_reason
那 function tool 通常最合适。
原因很直接:
- schema 明确
- 更适合 strict mode
- 更适合日志校验、审批和回放
5.2 Custom tool 适合不想强包 JSON 的文本型输入
OpenAI 当前官方文档把 custom tool 定义得很清楚:
- custom tool 允许模型返回任意字符串作为输入
- 也可以再叠加 grammar 约束
这类工具更适合:
- 生成代码片段
- 生成表达式
- 输入本身天然是文本而不是对象字段
但风险也更大,因为:
- 结构化验证更弱
- 更容易混入无关文本
- 更需要 grammar 或下游清洗兜底
5.3 Built-in tool 更像平台级执行层,不是你的业务函数
内建工具例如:
- web search
- MCP
- shell
- local shell
- computer use
它们回答的通常不是“你的业务系统要做什么动作”,而是:
- 模型怎样访问平台级能力或执行环境
所以不要把:
- 业务读写动作
- 平台执行能力
揉成同一层概念。
6. Strict mode、Structured Outputs 和 schema 质量
6.1 OpenAI 官方建议:function tool 尽量开 strict: true
OpenAI 当前 Function Calling 官方文档明确建议:
- function tool 的 strict mode 尽量始终开启
因为 strict mode 会利用 Structured Outputs,让模型对函数参数更可靠地遵循 schema,而不是“尽力而为”。
6.2 开 strict 不是只写个布尔值,还要满足 schema 前提
OpenAI 当前官方文档还写明了 strict mode 的关键要求:
- 每个 object 都要设置
additionalProperties: false properties里的字段都要在required里显式标记
也就是说,很多团队以为自己“开了严格模式”,其实 schema 本身还没准备好。
6.3 Function tool 的 schema 质量会直接决定工具稳定性
Structured Outputs 官方文档给出的建议很值得直接拿来用:
- key 要命名清晰
- title 和 description 要清楚
- 用 evals 去找最合适的结构
从工程角度看,这意味着 Tool schema 设计不是一次性工作,而是持续优化对象。
6.4 JSON 合法不等于契约可靠
很多团队看到模型能输出合法 JSON,就以为 Tool 稳了。
其实真正关键的是:
- 枚举是否合法
- 必填字段是否齐
- 值域是否合理
- 失败时是否有统一结构
合法 JSON 只是底线,不是上线标准。
7. Tool 最好怎么分层
最常见、也最实用的分层方式是四层:
7.1 原子只读 Tool
例如:
search_runbookget_ticketlist_recent_alerts
它们的特点是:
- 不产生副作用
- 更适合高频自动调用
- 更适合做缓存和结果压缩
7.2 原子写入 Tool
例如:
create_ticketupdate_order_statussend_notification
这类 Tool 要重点补:
- 幂等键
- 风险级别
- 审批要求
- 超时和补偿策略
7.3 两阶段写 Tool
高风险动作最好拆成:
draft_*execute_*
这样模型先产出执行草案,再由系统或人工确认后执行。
7.4 编排层不要伪装成单一 Tool
如果一个能力内部已经包含多步状态推进、回滚、审批和人机协同,它更像 workflow 或 skill,不再只是一个普通 Tool。
8. Tool 不只是“能调通”,还要“能收得住”
8.1 错误返回要可操作
不要只返回:
errorfailed
更适合工程治理的返回通常会说明:
- 失败类别
- 是否可重试
- 是否需要换参数
- 是否需要人工接管
例如:
| 字段 | 含义 |
|---|---|
error_code | 稳定错误分类 |
retryable | 是否允许自动重试 |
requires_approval | 是否要进入审批链 |
user_action_required | 是否需要补证据或补参数 |
8.2 写 Tool 必须回答重试问题
只要会产生副作用,就要提前说清:
- 自动重试是否安全
- 并发调用是否安全
- 超时后是否可能已经部分成功
- 如何补偿
这类问题如果不写进契约,就会在事故里用生产系统替你回答。
8.3 审批不是 UI 补丁,而是 Tool 契约的一部分
高风险 Tool 最好从定义层就表达:
- 风险级别
- 所需审批角色
- 执行前要展示哪些证据
- 审批后如何恢复运行
这比在页面上额外加一个“确定吗”按钮可靠得多。
8.4 Tool 结果最好分 raw、sanitized、model-facing 三层
虽然这部分在结构化输出专题里已经展开讲过,但从 Tool 视角也很关键。
比较稳的三层通常是:
raw:外部系统原始返回sanitized:做过脱敏、白名单、字段收缩model-facing:真正回灌给模型的最小结果
这样你才能同时满足:
- 审计
- 回放
- 模型稳定消费
8.5 有些结果应该拒绝传播,而不是继续总结
如果结果包含:
- 敏感数据
- 跨租户混入
- 注入性文本
- 高风险失败堆栈
更稳的做法通常不是“让模型自己总结一下”,而是:
- 直接阻断回灌
- 只回传结构化失败标签
- 转人工或审批链
9. Parallel tool calls、tool_choice 和执行约束
9.1 并发工具调用不是默认越多越好
OpenAI 当前 Function Calling 官方文档明确说明:
- 模型可能在一轮里选择多个函数
- 你可以通过
parallel_tool_calls: false限制成零个或一个
这条配置非常适合高风险工具或强状态依赖工具。
9.2 Built-in tools 场景下并行限制和 function tool 不完全一样
OpenAI 当前文档还明确说:
- 使用 built-in tools 时,不支持 parallel function calling 那套并发模式
这也再次说明:
- function tool
- built-in tool
不能简单套用同一套运行假设。
9.3 对写工具来说,串行通常比并行更稳
对于写操作,开启并发最常见的问题是:
- 多个副作用同时落地
- 回滚顺序变复杂
- 审批和证据链难配对
所以一个很实用的经验是:
- 读工具更适合考虑并行
- 写工具默认优先串行
9.4 tool_choice 是收束风险的重要开关
虽然很多系统喜欢让模型自由选工具,但在某些阶段你更适合显式控制:
- 必须不用工具
- 必须用某个工具
- 只能在已加载子集里选
特别是在 tool search 后只加载了某个子集时,tool_choice 可以帮助你把模型收在更小的工具面里。
10. Tool surface 太大时,不要靠 prompt 撑住
10.1 OpenAI 官方建议:大工具面优先配 tool search
OpenAI 当前 Function Calling 和 Tool Search 官方文档都明确写到:
- 当函数很多、schema 很大时,可以配 tool search
- 只有
gpt-5.4及以后支持tool_search
10.2 defer_loading 解决的是上下文和成本问题
Tool Search 官方文档给出的关键机制是:
- 对 function 设置
defer_loading: true - 或对 MCP server 设置
defer_loading: true - 再把
tool_search加进tools
这样模型起步时只看到:
- 名称
- 描述
而不是所有细节 schema 都先塞进上下文。
10.3 Namespace 比“平铺 100 个函数”更适合搜索
OpenAI 当前 Tool Search 官方建议很清楚:
- 尽可能用 namespace
- namespace 描述要清晰
- 最好把 namespace 控制在较小规模
对 Tool 设计的启发是:
- 工具分组本身就是工程设计的一部分
- 不要等到工具炸到上百个再想起分命名空间
10.4 Hosted tool search 和 client-executed tool search 解决的问题不同
Tool Search 官方文档还区分了两种路径:
- hosted tool search:已知候选工具全集时最省心
- client-executed tool search:当工具发现依赖项目态、租户态或你自己的系统时更灵活
这意味着不要笼统说“我们用了 tool search”,而要讲清:
- 搜索是谁执行的
- loaded subset 怎么回灌
- search result 是不是可信
11. Observability、回放和评测字段不能等事故后再补
11.1 至少记录这些最小观测字段
一个可回放的 tool call 事件,至少应该保留:
trace_idresponse_idcall_id- tool
name - tool
namespace - 参数快照
- 输出快照
- 状态
- 耗时
- 错误分类
11.2 Tool 评测不该只看“最后任务是否成功”
从 Tool 视角,至少还应该单独看:
- 工具选择正确率
- 参数合法率
- 非必要工具调用率
- 工具重试率
- 高风险动作审批命中率
- 失败后假装成功率
如果只看最终成功率,很容易漏掉:
- 路径错了但最后看起来还能说通
- 写工具风险过高但被结果掩盖
- 工具成本和错误恢复正在恶化
11.3 Tool output 最好可回放,而不是只剩最终摘要
出了问题以后,团队真正需要的是:
- 当时给了什么定义
- 模型发了什么参数
- 工具真实返回了什么
- 哪一步开始被污染
所以 Tool 系统一定要先对“回放”友好,再谈“摘要看起来多漂亮”。
12. Tool 和 MCP、Skill 到底怎么区分
这里最容易混:
Tool讲的是“系统能执行什么动作”MCP讲的是“这些动作如何按统一协议暴露出去”Skill讲的是“在某类任务里如何组织这些动作、上下文和策略”
一句话记忆:
Tool是能力接口MCP是接入协议层Skill是场景封装层
如果你发现自己在讨论:
- 参数 schema
- 输出结构
- 幂等与副作用
那你讨论的是 Tool。
13. 什么时候该优先补 Tool,而不是继续调 Prompt
如果线上问题是这些,通常先看 Tool,不是先堆 Prompt:
- 模型经常选错工具
- 参数总是填错字段
- 同类失败不断重试
- 工具返回太脏导致后续推理漂移
- 高风险动作没有清晰审批边界
因为这类问题本质上不是“模型没听懂”,而是“能力接口本身不适合稳定调用”。
13.1 一个很常见的反模式:schema 有问题,却继续补自然语言说明
OpenAI 当前 Structured Outputs 官方文档里其实已经把方向讲得很清楚:
- 能用 schema 解决的,优先用 schema 解决
- 不要只靠越来越长的 prompt 去弥补契约缺陷
这条在 Tool 设计里尤其关键,因为很多工具问题本质上是:
- 字段设计错
- 枚举设计错
- 结果结构错
而不是“说明文字还不够多”。
14. Tool 的落地检查清单
- 是否把后端 API 重新收敛成模型可理解的最小能力单元
- 是否区分了只读、写入和高风险动作
- 是否同时定义了输入 schema、输出 schema 和错误结构
- 是否尽量开启了
strict: true并满足 strict mode 的 schema 前提 - 是否写清了幂等、并发、超时和重试语义
- 是否为 tool output 设计了 raw / sanitized / model-facing 三层
- 是否能在失败后给出可操作的处理信号
- 是否为高风险动作预留审批、确认和补偿机制
- 是否为大工具面考虑了 namespace、
defer_loading和 tool search - 是否记录了稳定的 tool call 观测字段和审计字段
15. 推荐联读
16. 参考资料
- OpenAI Tools guide:https://developers.openai.com/api/docs/guides/tools
- OpenAI Function Calling guide:https://developers.openai.com/api/docs/guides/function-calling
- OpenAI Structured Outputs guide:https://developers.openai.com/api/docs/guides/structured-outputs
- OpenAI Tool Search guide:https://developers.openai.com/api/docs/guides/tools-tool-search
- OpenAI MCP and Connectors guide:https://developers.openai.com/api/docs/guides/tools-connectors-mcp